Skip to content

feat: walkthrough documents with reading progress and GameFAQs import#3816

Open
gantoine wants to merge 1 commit into
masterfrom
feat/walkthrough-documents
Open

feat: walkthrough documents with reading progress and GameFAQs import#3816
gantoine wants to merge 1 commit into
masterfrom
feat/walkthrough-documents

Conversation

@gantoine

Copy link
Copy Markdown
Member

Description
Explain the changes or enhancements you are proposing with this pull request.

Adds walkthrough documents to RomM and unifies them with manuals on a single document model, rather than building walkthroughs as a separate subsystem (an alternative approach to #2892).

The key idea: manuals already have a many-per-ROM substrate (rom_files with category=manual). Walkthroughs become rom_files under a new walkthrough category, with an optional RomFileDocMeta provenance sidecar, and reading progress is a per-user RomFileUser row keyed on rom_file_id — so one mechanism covers manuals and walkthroughs. The legacy scraped-manual columns on roms are untouched.

Backend

  • New RomFileCategory.WALKTHROUGH, DocSource enum, RomFileDocMeta sidecar (mirrors TrackMeta), RomFileUser progress table (mirrors RomUser).
  • Migration 0098_walkthrough_docs — enum value + two tables, cross-DB (Postgres/MySQL/MariaDB); upgrade→downgrade→re-upgrade verified.
  • Serve .txt/.html documents inline; HTML is served under a sandboxing Content-Security-Policy. .txt added to allowed manual files.
  • Endpoints: walkthrough upload / GameFAQs fetch / delete, plus per-user reading-progress GET/PUT (uses ROMS_READ, since progress is personal state like notes).
  • GameFAQs import extracts plain text only (stored as .txt) — no remote HTML is ever persisted or served, so sanitization is by construction. Requests are host-allowlisted on top of the existing SSRF-protected client.

Frontend (v2)

  • New TextViewer (.txt/.html), useReadingProgress composable, WalkthroughSubtab (upload + add-from-GameFAQs-URL + delete + reading-progress bar), wired into MediaTab.
  • ManualSubtab now renders .txt manuals; the walkthrough category is surfaced in FilesTab. Types regenerated; i18n keys added across all locales.

AI assistance disclosure
Implemented with Claude Code (Opus 4.8). The data-model design, backend, migration, tests, and v2 UI were AI-authored under human direction. Per RomM's contribution policy.

Checklist
Please check all that apply.

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

Note: backend pytest and frontend vitest/vue-tsc/trunk all pass. The v2 UI is code-complete pending browser QA — it has not yet been visually verified in both themes and all input modalities (mouse/touch/keyboard/gamepad), per the repo's UI verification bar. Reading progress is wired for text documents; PDF/Markdown scroll-position persistence is a follow-up (the composable is viewer-agnostic and ready for it).

Screenshots (if applicable)

Pending browser QA.

Unify manuals and walkthroughs on one document model instead of a
separate walkthrough subsystem. Walkthroughs are RomFiles under a new
WALKTHROUGH category with an optional RomFileDocMeta provenance sidecar,
and reading progress is a per-user RomFileUser row keyed on rom_file_id
so the same mechanism covers manuals and walkthroughs.

Backend:
- models: RomFileCategory.WALKTHROUGH, DocSource, RomFileDocMeta,
  RomFileUser; migration 0098 (enum value + two tables, cross-DB)
- serve .txt/.html documents inline (HTML under a sandboxing CSP);
  add .txt to allowed manual files
- endpoints: walkthrough upload / GameFAQs fetch / delete, and per-user
  reading-progress GET/PUT (ROMS_READ, like notes)
- GameFAQs import extracts text only (stored as .txt); no remote HTML is
  persisted or served. Host-allowlisted on top of the SSRF-safe client
- tests for CRUD, progress, and the GameFAQs parser/URL validation

Frontend (v2):
- TextViewer (.txt/.html), useReadingProgress composable, WalkthroughSubtab
  (upload + add-from-GameFAQs-URL + delete + progress), wired into MediaTab
- ManualSubtab renders .txt manuals; walkthrough category surfaced in
  FilesTab; regenerated types; i18n keys across all locales

AI assistance: implemented with Claude Code (Opus 4.8). The data-model
design, backend, migration, tests, and v2 UI were AI-authored under human
direction. Alternative approach to #2892.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 18, 2026 23:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds walkthrough documents and shared reading progress for manuals and walkthroughs. The main changes are:

  • New document metadata and per-user progress tables.
  • Walkthrough upload, GameFAQs import, deletion, and content endpoints.
  • Inline text and sandboxed HTML document viewing.
  • New v2 walkthrough, text viewer, and progress UI.
  • Generated API types, file categories, tests, and translations.

Confidence Score: 4/5

Hidden-ROM mutations, failed replacement uploads, and lost progress saves need fixes before merging.

  • Walkthrough mutations skip the established ROM visibility boundary.
  • Interrupted replacement uploads can delete a previously valid document.
  • Database failures can leave filesystem and database state out of sync.
  • Concurrent progress creation can fail on the unique key.
  • Switching documents can discard the latest pending reading position.

backend/endpoints/roms/walkthrough.py, backend/handler/database/roms_handler.py, frontend/src/v2/composables/useReadingProgress/index.ts

Security Review

The walkthrough upload, GameFAQs import, and delete routes require write scope but skip the existing per-ROM visibility check. A user who can identify a hidden ROM ID can modify that ROM or remove its walkthrough files. Inline HTML is constrained by CSP and iframe sandboxing, and the GameFAQs client retains the existing SSRF protections.

Important Files Changed

Filename Overview
backend/endpoints/roms/walkthrough.py Adds walkthrough mutations and GameFAQs import, with visibility and filesystem consistency gaps.
backend/endpoints/roms/files.py Adds guarded document serving and per-user reading-progress endpoints.
backend/handler/database/roms_handler.py Adds document metadata and progress persistence, including a race-prone read-then-insert upsert.
backend/handler/walkthrough/gamefaqs.py Adds allowlisted GameFAQs fetching and plain-text guide extraction.
backend/alembic/versions/0098_walkthrough_docs.py Adds the walkthrough enum value and document metadata and progress tables.
backend/models/rom.py Adds document provenance, walkthrough category, and per-file user progress models.
frontend/src/v2/composables/useReadingProgress/index.ts Adds debounced progress persistence but drops pending state during document switches.
frontend/src/v2/components/GameDetails/TextViewer.vue Adds plain-text rendering, sandboxed HTML viewing, and progress integration.
frontend/src/v2/components/GameDetails/WalkthroughSubtab.vue Adds walkthrough selection, upload, import, deletion, and viewing controls.

Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat: walkthrough documents with reading..." | Re-trigger Greptile

Comment on lines +86 to +89
rom = db_rom_handler.get_rom(id)
if not rom:
raise RomNotFoundInDatabaseException(id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Hidden ROM Mutations Bypass Visibility

These routes resolve the ROM but never call assert_rom_visible. A user with ROMS_WRITE who knows a hidden ROM ID can upload or import a walkthrough, and the delete route has the same gap, allowing files on a hidden ROM to be modified or removed.

Fix in Claude Code

Comment on lines +126 to +127
await fs_rom_handler.make_directory(walkthrough_dir_rel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed Re-upload Deletes Existing File

FileTarget writes directly to the final path, including when a walkthrough with that name already exists. If the client disconnects or multipart parsing fails, the error handler unlinks that path, so an interrupted replacement permanently removes the previous valid document while its database row remains.

Fix in Claude Code

Comment on lines +267 to +286
created = db_rom_handler.add_rom_file(
RomFile(
rom_id=rom.id,
file_name=file_name,
file_path=walkthrough_dir_rel,
file_size_bytes=stat.st_size,
last_modified=stat.st_mtime,
category=RomFileCategory.WALKTHROUGH,
)
)
db_rom_handler.upsert_doc_meta(
rom_file_id=created.id,
rom_id=rom.id,
values={
"source": DocSource.GAMEFAQS,
"source_url": url,
"author": guide["author"],
"title": guide["title"],
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Database Failure Orphans Written Guide

The guide is written before its RomFile and metadata rows are created. If either database operation fails, the request returns an error but leaves an unindexed file or a file row without its intended provenance metadata; the upload route has the same filesystem-first failure path.

Fix in Claude Code

Comment on lines +1834 to +1857
@begin_session
def upsert_rom_file_user(
self,
rom_file_id: int,
user_id: int,
values: dict,
session: Session = None, # type: ignore
) -> RomFileUser:
"""Create or update a user's reading progress for a document file."""
existing = session.scalar(
select(RomFileUser)
.filter_by(rom_file_id=rom_file_id, user_id=user_id)
.limit(1)
)
if existing:
for key, val in values.items():
setattr(existing, key, val)
session.flush()
return existing

state = RomFileUser(rom_file_id=rom_file_id, user_id=user_id, **values)
session.add(state)
session.flush()
return state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Concurrent Progress Inserts Collide

This upsert performs a read followed by an insert without locking or handling the unique constraint. Two saves for the same user and file, such as concurrent browser tabs, can both observe no row and then race to insert, causing one progress request to fail instead of updating the existing state.

Fix in Claude Code

Comment on lines +75 to +83
watch(fileId, () => {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
progress.value = 0;
void restore();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Document Switch Discards Pending Progress

When fileId changes within the debounce window, this watcher clears the old document's timer without saving its pending position. The viewer then restores the new document, so the user's latest position in the previous document is permanently lost; the pending value must retain its original ROM and file identity and be flushed before switching.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@gantoine gantoine added the on-hold Pending further research or blocked by another issue label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on-hold Pending further research or blocked by another issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants